﻿' Copyright © Microsoft Corporation.  All Rights Reserved.
' This code released under the terms of the 
' Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)
' <copyright file="PlaylistCollection.vb" company="Microsoft">
'     Copyright © Microsoft Corporation. All rights reserved.
' </copyright>
' <summary>Implements the PlaylistCollection class</summary>
' <author>Microsoft Expression Encoder Team</author>


Imports Microsoft.VisualBasic
Imports Microsoft.Expression.Encoder.PlugInLoader
Imports Microsoft.Expression.Encoder.PlugInMssCtrl
Imports System
Imports System.ComponentModel
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Diagnostics
Imports System.Globalization
Imports System.IO
Imports System.IO.IsolatedStorage
Imports System.Text
Imports System.Threading
Imports System.Windows
Imports System.Windows.Browser
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Xml

Namespace ExpressionMediaPlayer
	''' <summary>
	''' exception for errors when taking the playlist offline
	''' </summary>
	Public Class PlaylistTakeOfflineException
		Inherits Exception
		Public Sub New()
			MyBase.New(Resources.errorInvalidPlaylist)
		End Sub

		Public Sub New(ByVal message As String)
			MyBase.New(message)
		End Sub

		Public Sub New(ByVal message As String, ByVal exp As Exception)
			MyBase.New(message, exp)
		End Sub

		Public Sub New(ByVal nodeFound As String, ByVal nodeExpected As String)
			MyBase.New(String.Format(CultureInfo.CurrentUICulture, Resources.errorInvalidPlaylistNode, nodeFound, nodeExpected))
		End Sub
	End Class




	''' <summary>
	''' event args for downloading playlist events
	''' </summary>
	Public Class PlaylistDownloadProgressEventArgs
		Inherits EventArgs
		Public Sub New(ByVal dblProgress As Double)
			Progress = dblProgress
		End Sub

		Private privateProgress As Double
		Public Property Progress() As Double
			Get
				Return privateProgress
			End Get
			Private Set(ByVal value As Double)
				privateProgress = value
			End Set
		End Property
	End Class

	Friend Enum QueueItemTask
		downloadFile
		initSmoothStreamingSupport
		processManifest
	End Enum

	Friend Class QueueItem
		Implements IDisposable
		Friend task As QueueItemTask
		Friend uriToDownload As IsoUri
        Friend playlistItem As PlaylistItem
        Friend retryCountValue As Integer = 0
        Friend Const MaxRetries As Integer = 3

        Friend Sub New(ByVal task As QueueItemTask, ByVal uri As Uri)
            Me.task = task
            Me.SetUri(uri)
        End Sub

        Friend Sub New(ByVal task As QueueItemTask, ByVal playlistItemParameter As PlaylistItem)
            Me.task = task
            Me.playlistItem = playlistItemParameter
            Me.SetUri(playlistItemParameter.MediaSource)
        End Sub

        Friend Sub SetUri(ByVal uri As Uri)
            Dim absoluteUri As Uri = uri
            If (Not absoluteUri.IsAbsoluteUri) AndAlso HtmlPage.Document.DocumentUri IsNot Nothing Then
                absoluteUri = New Uri(HtmlPage.Document.DocumentUri, uri)
            End If
            Me.uriToDownload = New IsoUri(absoluteUri)
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            Me.uriToDownload.Dispose()
            GC.SuppressFinalize(Me)
        End Sub

        Friend ReadOnly Property RetryCount() As Integer
            Get
                Return Me.retryCountValue
            End Get
        End Property

        Friend Function BumpRetryCount() As Integer
            Me.retryCountValue += 1
#If DEBUG Then
            MessageTracer.TraceMessage("Retry:" & Me.retryCountValue.ToString() & " task: " & Me.task.ToString() & " url=" & Me.uriToDownload.ToString())
#End If
            Return Me.retryCountValue
        End Function

		Friend Function DoTask(ByVal playlist As Playlist) As Boolean
#If DEBUG Then
            MessageTracer.TraceMessage("DoTask: retry=" & Me.retryCountValue.ToString() & " now=" & Date.Now.ToString() & " task: " & Me.task.ToString() & " url=" & Me.uriToDownload.ToString())
#End If
			Select Case Me.task
				Case QueueItemTask.downloadFile
					AddHandler uriToDownload.UriDownloadProgressChanged, AddressOf playlist.PlaylistCollection_UriDownloadProgressChanged
					Me.uriToDownload.Download()
					Return Me.uriToDownload.DownloadSucceeded
				Case QueueItemTask.initSmoothStreamingSupport
					Return playlist.InitSmoothStreamingSupport(Me.uriToDownload)
				Case QueueItemTask.processManifest
					Return playlist.ProcessDownloadedOfflineManifest(Me.playlistItem, Me.uriToDownload)
			End Select
			Throw New PlaylistTakeOfflineException("Develoer Error!: new task member added to QueueItemTask enum but not added the DoTask() Switch statement!")
		End Function
	End Class

#If DEBUG Then
	Friend NotInheritable Class MessageTracer
		Private Shared m_player As MediaPlayer

		Private Sub New()
		End Sub
		Friend Shared Sub SetPlayer(ByVal player As MediaPlayer)
			m_player = player
		End Sub

		Friend Shared Sub TraceMessage(ByVal message As String)
			Debug.WriteLine(message)
			If m_player IsNot Nothing Then
				m_player.SetNextTickErrorMessage(message)
			End If
		End Sub
	End Class
#End If

	Partial Public Class Playlist
		Inherits DependencyObject
		Private m_progressDialog As OfflineDownloadProgressDialog
		Private m_player As MediaPlayer
		Private playerSize As Size
		Private m_smoothStreamingSourcePlugIn As PlugInLoader
		Private m_baseUriForTakingOffline As Uri

		Private m_smoothStreamingObject As Object
		''' <summary>
		''' event for download progress of playlist items.
		''' </summary>
		Public Event PlaylistDownloadProgressChanged As EventHandler(Of PlaylistDownloadProgressEventArgs)


		''' <summary>
		''' is playlist currently downloading?
		''' </summary>
		Private privateIsDownloading As Boolean
		Friend Property IsDownloading() As Boolean
			Get
				Return privateIsDownloading
			End Get
			Private Set(ByVal value As Boolean)
				privateIsDownloading = value
			End Set
		End Property

		''' <summary>
		''' items to download if taking this playlist offline
		''' </summary>
		Private _downloadItems As List(Of QueueItem) = Nothing

		''' <summary>
		''' Add a work item to the Queue
		''' </summary>
		''' <param name="strItem"></param>
		Private Sub EnqueueItem(ByVal item As QueueItem)
			If _downloadItems.IndexOf(item) = -1 Then
#If DEBUG Then
				MessageTracer.TraceMessage("QueueItem: task=" & item.task.ToString() & " uri=" & item.uriToDownload.ToString() & " isoUri=" & item.uriToDownload.StreamName)
#End If
				_downloadItems.Add(item)
				If Me.m_progressDialog IsNot Nothing Then
					Me.m_progressDialog.TotalItemCount += 1
				End If
#If DEBUG Then
			Else
				MessageTracer.TraceMessage("QueueItem Item already in queue:task=" & item.task.ToString() & " uri=" & item.uriToDownload.ToString())
#End If
			End If
		End Sub

		''' <summary>
		''' add a mediaitem (image or video/audio) to list of items to download
		''' </summary>
		''' <param name="strItem"></param>
		Private Sub QueueFileItemForDownload(ByVal uri As Uri)
			If uri IsNot Nothing AndAlso (Not String.IsNullOrEmpty(uri.ToString())) Then
				Dim item As New QueueItem(QueueItemTask.downloadFile, uri)
				EnqueueItem(item)
#If DEBUG Then
			Else
				MessageTracer.TraceMessage("QueueItemForDownload funky parameter passed in!!!")
#End If
			End If
		End Sub

		''' <summary>
		''' adds all the adaptive chunk items to the queue
		''' </summary>
		''' <param name="playlistItem"></param>
		Friend Function ProcessDownloadedOfflineManifest(ByVal playlistItem As PlaylistItem, ByVal manifest As IsoUri) As Boolean
#If DEBUG Then
			MessageTracer.TraceMessage("ProcessDownloadedOfflineManifest")
#End If
			If manifest.IsoFileExists Then
				Dim plugInMSSCore As IPlugInMssCore = TryCast(Me.m_smoothStreamingObject, IPlugInMssCore)
				Dim pluginOfflineSupport As IPlugInMssOfflineSupport = TryCast(Me.m_smoothStreamingObject, IPlugInMssOfflineSupport)
				If plugInMSSCore IsNot Nothing AndAlso pluginOfflineSupport IsNot Nothing Then
					plugInMSSCore.ManifestUrl = manifest.FullyQualifiedUri ' Provide host URL so chunk URLs refer to the host
					pluginOfflineSupport.ParseManifest(manifest.Stream) ' But parse the local copy of manifest al ready on hand

					Dim recomendedBitrateInKbpsAudio As Long = pluginOfflineSupport.RecommendBitrateInKbps(MediaStreamType.Audio, Me.playerSize)
					Dim chunkUrlsAudio As ReadOnlyCollection(Of String) = pluginOfflineSupport.GetChunkUrls(MediaStreamType.Audio, recomendedBitrateInKbpsAudio)

					Dim recomendedBitrateInKbpsVideo As Long
					If playlistItem.OfflineVideoBitrateInKbps > 0 Then
						recomendedBitrateInKbpsVideo = playlistItem.OfflineVideoBitrateInKbps
					Else
						recomendedBitrateInKbpsVideo = pluginOfflineSupport.RecommendBitrateInKbps(MediaStreamType.Video, Me.playerSize)
						playlistItem.OfflineVideoBitrateInKbps = recomendedBitrateInKbpsVideo
					End If
					Dim chunkUrlsVideo As ReadOnlyCollection(Of String) = pluginOfflineSupport.GetChunkUrls(MediaStreamType.Video, recomendedBitrateInKbpsVideo)

					Dim chunkIndexAudio As Integer = 0
					Dim chunkIndexVideo As Integer = 0
					Do While (chunkIndexAudio < chunkUrlsAudio.Count) OrElse (chunkIndexVideo < chunkUrlsVideo.Count)
						If chunkIndexVideo < chunkUrlsVideo.Count Then
							Dim chunkUrl As String = chunkUrlsVideo(chunkIndexVideo)
							chunkIndexVideo += 1
#If DEBUG Then
							MessageTracer.TraceMessage("Video ChunkUrl: " & chunkUrl)
#End If
							Dim item As New QueueItem(QueueItemTask.downloadFile, New Uri(chunkUrl, UriKind.RelativeOrAbsolute))
							EnqueueItem(item)
						End If
						If chunkIndexAudio < chunkUrlsAudio.Count Then
							Dim chunkUrl As String = chunkUrlsAudio(chunkIndexAudio)
							chunkIndexAudio += 1
#If DEBUG Then
							MessageTracer.TraceMessage("Audio ChunkUrl: " & chunkUrl)
#End If
							Dim item As New QueueItem(QueueItemTask.downloadFile, New Uri(chunkUrl, UriKind.RelativeOrAbsolute))
							EnqueueItem(item)
						End If
					Loop
					Return True
				End If
			End If
			Return False
		End Function


		''' <summary>
		''' seperate thread to download all the queued media items.
		''' </summary>
		''' <param name="arg"></param>
		Private Sub DownloadThreadProc()
			Dim countLastLoop As Integer = 0

			Do While _downloadItems.Count > 0
				' Add the size of the new items to total size count
				Dim countThisLoop As Integer = _downloadItems.Count
				If countThisLoop >= countLastLoop Then
#If DEBUG Then
					MessageTracer.TraceMessage("Recalculating size of downloaded items")
#End If
				End If
				countLastLoop = countThisLoop

				' Pull next item from the queue
				Dim item As QueueItem = _downloadItems(0)
				_downloadItems.RemoveAt(0)

				' Do the task
				If Not item.DoTask(Me) Then
					If item.RetryCount < QueueItem.MaxRetries Then
						item.BumpRetryCount()
						_downloadItems.Insert(0,item)
					Else
						If Me.m_progressDialog IsNot Nothing Then
							Me.m_progressDialog.ReportError(item.uriToDownload.DownloadErrorMessage)
						End If
						Exit Do
					End If
				End If

				Me.m_progressDialog.CurrentItem += 1
				If Me.m_progressDialog.CancelWasClicked Then
					Exit Do
				End If
			Loop

			RemoveHandler PlaylistDownloadProgressChanged, AddressOf m_progressDialog.DownloadProgressChanged

			Me.IsDownloading = False

			If Me.m_progressDialog.CancelWasClicked Then
				Me.m_player.Dispatcher.BeginInvoke (Function() AnonymousMethod1())

			Else
				'''
				''' copy playlist XML to isolated storage...
				'''
				WriteToIsoStorage()
				'''
				''' write marker file (also contains baseUri) to isolated storage...
				'''
				WriteDownloadCompleteFile()

				If Me.m_player IsNot Nothing Then
					Me.m_player.Dispatcher.BeginInvoke (Function() AnonymousMethod2())
				End If
			End If
		End Sub
		
		Private Function AnonymousMethod1() As Object
			Me.m_progressDialog.Close()
			Return Nothing
		End Function
		
		Private Function AnonymousMethod2() As Object
			Me.m_progressDialog.DownloadCompletedSuccessfully()
			Return Nothing
		End Function

		Friend Const downloadCompletedFilename As String = "DownloadCompleted.txt"

		Friend Shared Function ContentNeedsDownloading() As Boolean
			Dim isoStore As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()
			Try
				If isoStore.FileExists(downloadCompletedFilename) Then
					Return False
				End If
			Catch ise As IsolatedStorageException
				Debug.WriteLine("IsolatedStorageException checking for download completed file" & ise.ToString())
				isoStore.Remove()
			End Try
			Return True
		End Function

		Private Sub WriteDownloadCompleteFile()
			If Me.m_baseUriForTakingOffline.IsAbsoluteUri Then
				Dim isoStore = IsolatedStorageFile.GetUserStoreForApplication()
				Dim streamWriter = New StreamWriter(isoStore.CreateFile(downloadCompletedFilename), Encoding.Unicode)
				streamWriter.WriteLine(Me.m_baseUriForTakingOffline.AbsoluteUri)
				streamWriter.Close()
#If DEBUG Then
				Dim msg As String = "Files in isolatedstorage: " & isoStore.GetFileNames().Length.ToString() & vbCrLf & "Space Left:" & isoStore.AvailableFreeSpace.ToString() & " of " & isoStore.Quota.ToString() & vbCrLf & downloadCompletedFilename
				MessageTracer.TraceMessage(msg)
#End If
			Else
				Throw New ArgumentException("must have an absolute Uri to tak offline")
			End If
		End Sub

		''' <summary>
		''' update download progress
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		Friend Sub PlaylistCollection_UriDownloadProgressChanged(ByVal sender As Object, ByVal e As UriDownloadProgressRoutedEventArgs)
			RaiseEvent PlaylistDownloadProgressChanged(Me, New PlaylistDownloadProgressEventArgs(e.Progress))
		End Sub

		Friend Shared Sub ClearPlaylistItemsFromIsolatedStorage()
			IsolatedStorageFile.GetUserStoreForApplication().Remove()
		End Sub

		Friend spaceNeededForOfflinePlaylistCache As Long = -1

		Friend Function ComputeSpaceNeededForOfflinePlaylist() As Long
			If Me.spaceNeededForOfflinePlaylistCache > 0 Then
				Return Me.spaceNeededForOfflinePlaylistCache
			End If

			'
			' calculate space
			'
			Dim totalSpaceRequired As Long = 1 << 20 ' 1Mb for playlist etc.
			Dim anyUnknown As Boolean = False
			For Each playlistItem As PlaylistItem In Items
				If playlistItem.FileSize <= 0 Then
					anyUnknown = True
#If DEBUG Then
					MessageTracer.TraceMessage("Figuring item Size unknown size:" & playlistItem.MediaSource.ToString())
#End If
				Else
					Dim itemSize = playlistItem.FileSize
#If DEBUG Then
					MessageTracer.TraceMessage("Figuring item Size:" & playlistItem.MediaSource.ToString() & " Size:" & itemSize.ToString())
#End If
					totalSpaceRequired += itemSize
				End If
			Next playlistItem

			'
			' if we cant determine it we try for at least 50Mb...
			'
			If anyUnknown Then
#If DEBUG Then
				MessageTracer.TraceMessage("Adding 50MB to buffer for size of unknown items")
#End If
				totalSpaceRequired = Math.Max(totalSpaceRequired, 50 * 1024 * 1024)
			End If

#If DEBUG Then
			MessageTracer.TraceMessage("Estimated Storage size:" & totalSpaceRequired.ToString())
#End If
			Me.spaceNeededForOfflinePlaylistCache = totalSpaceRequired
			Return totalSpaceRequired
		End Function

		Friend Function AdditionalSpaceQuotaRequired() As Long
			Dim totalSpaceRequired As Long = ComputeSpaceNeededForOfflinePlaylist()
			Dim isoStore As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()
			Dim spaceQuota As Long = isoStore.Quota
#If DEBUG Then
			MessageTracer.TraceMessage("Quota size:" & spaceQuota.ToString())
#End If
			Dim aditionalQuotaRequired As Long = (totalSpaceRequired - spaceQuota)
#If DEBUG Then
			MessageTracer.TraceMessage("aditionalQuotaRequired:" & aditionalQuotaRequired.ToString())
#End If
			Return aditionalQuotaRequired
		End Function

		''' <summary>
		''' ensure enough iso storage space for files we wish to copy
		''' </summary>
		''' <returns></returns>
		Friend Function EnsureStorageSpace() As Boolean
			ClearPlaylistItemsFromIsolatedStorage()

			Dim moreSpaceNeeded As Long = AdditionalSpaceQuotaRequired()
			If moreSpaceNeeded > 0 Then
				Dim isoStore As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()
				If Not isoStore.IncreaseQuotaTo(ComputeSpaceNeededForOfflinePlaylist()) Then
#If DEBUG Then
					MessageTracer.TraceMessage("Failed to increase quota!")
#End If
					Return False
				End If
			End If
			Return True
		End Function
		''' <summary>
		''' make a local isolated storage cache of this playlist
		''' </summary>
		Public Sub TakeOffline(ByVal player As MediaPlayer)
			If Me.IsDownloading Then
				Throw New PlaylistTakeOfflineException("TakeOffline called while already downloading")
			End If

			If (Not HtmlPage.IsEnabled) OrElse HtmlPage.Document Is Nothing OrElse HtmlPage.Document.DocumentUri Is Nothing Then
				Throw New PlaylistTakeOfflineException("TakeOffline called with HtmlPage in invalid state")
			End If

			Me.m_player = player
			Me.playerSize = New Size(Me.m_player.ActualWidth, Me.m_player.ActualHeight)
			Me.m_progressDialog = New OfflineDownloadProgressDialog(Me.m_player)
			Me.m_progressDialog.Show()
			AddHandler PlaylistDownloadProgressChanged, AddressOf m_progressDialog.DownloadProgressChanged

			Dim xapUriRelative As New Uri(MediaPlayer.smoothStreamingXAPName, UriKind.Relative)
			Dim xapUriFull As New Uri(HtmlPage.Document.DocumentUri, xapUriRelative)
			Dim baseUrl As String = xapUriFull.AbsoluteUri.Replace(MediaPlayer.smoothStreamingXAPName, String.Empty)
			Me.m_baseUriForTakingOffline = New Uri(baseUrl, UriKind.Absolute)

#If DEBUG Then
			MessageTracer.SetPlayer(player)
#End If

			Me.IsDownloading = True

			Me._downloadItems = New List(Of QueueItem)()

			Dim smoothStreamingSupportLoaded As Boolean = False

			'''
			''' queue media items for download to isolated storage
			'''
			For Each playlistItem As PlaylistItem In Items
				QueueFileItemForDownload(playlistItem.ThumbSource)
				For Each chapterItem As ChapterItem In playlistItem.Chapters
					QueueFileItemForDownload(chapterItem.ThumbSource)
				Next chapterItem
				QueueFileItemForDownload(playlistItem.MediaSource)
				If playlistItem.IsAdaptiveStreaming Then
					If Not smoothStreamingSupportLoaded Then
						smoothStreamingSupportLoaded = True

						Dim uriXAP As New Uri(MediaPlayer.smoothStreamingXAPName, UriKind.Relative)

						Dim downloadSmoothStreamingSupport As New QueueItem(QueueItemTask.downloadFile, uriXAP)
						EnqueueItem(downloadSmoothStreamingSupport)

						Dim smoothStreamingSupportItem As New QueueItem(QueueItemTask.initSmoothStreamingSupport, uriXAP)
						EnqueueItem(smoothStreamingSupportItem)
					End If
					Dim processManifestItem As New QueueItem(QueueItemTask.processManifest, playlistItem)
					EnqueueItem(processManifestItem)
				End If
			Next playlistItem

			'''
			''' copy media items to isolated storage
			'''
			Dim thread As New Thread(AddressOf DownloadThreadProc)
			thread.Start()
		End Sub

		''' <summary>
		''' initialize
		''' </summary>
		Friend Function InitSmoothStreamingSupport(ByVal xap As IsoUri) As Boolean
#If DEBUG Then
			MessageTracer.TraceMessage("InitSmoothStreamingSupport")
#End If
			If xap.IsoFileExists Then
				If m_smoothStreamingSourcePlugIn Is Nothing Then
					Me.m_smoothStreamingSourcePlugIn = New PlugInLoader(Me.m_player)
					Me.m_smoothStreamingSourcePlugIn.LocalLoad(xap.Stream, MediaPlayer.smoothStreamingAssemblyName)
				End If
				If m_smoothStreamingSourcePlugIn IsNot Nothing AndAlso Me.m_smoothStreamingSourcePlugIn.Ready Then
					Me.m_smoothStreamingObject = Me.m_smoothStreamingSourcePlugIn.CreateObject(MediaPlayer.smoothStreamingOnlineObjectName)
					Return True
				End If
			End If
			Return False
		End Function

		#Region "OfflineSerializationSupport"
		''' <summary>
		''' top level XML node
		''' </summary>
		Private Const s_PlaylistIsoName As String = "Playlist.xml"
		''' <summary>
		''' persist playlist in offline storage
		''' </summary>
		Public Sub WriteToIsoStorage()
			Using isoStore As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()
				Dim xmlws As New XmlWriterSettings()
				xmlws.Indent = True
				Using isoStream As IsolatedStorageFileStream = isoStore.CreateFile(s_PlaylistIsoName)
				Using writer As XmlWriter = XmlWriter.Create(isoStream, xmlws)
					Me.Serialize(writer)
				End Using
				End Using
			End Using
		End Sub
		''' <summary>
		''' load this playlist from the offline storage
		''' </summary>
		''' <returns></returns>
		Public Shared Function ReadFromIsoStorage() As Playlist
		   Dim xmlrs As New XmlReaderSettings()
		   xmlrs.IgnoreComments = True
		   xmlrs.IgnoreWhitespace = True
		   Using isoStore As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()
			   Using isoStream As IsolatedStorageFileStream = isoStore.OpenFile(s_PlaylistIsoName, FileMode.Open)
			   Using reader As XmlReader = XmlReader.Create(isoStream, xmlrs)
				   Dim playlist As New Playlist()
				   playlist.Deserialize(reader)
				   Return playlist
			   End Using
			   End Using
		   End Using
		End Function
		#End Region
	End Class
End Namespace
